package org.keyintegrity.webbeans.utils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;

import org.apache.hivemind.ApplicationRuntimeException;
import org.apache.log4j.Logger;

public class BeanUtils {

    private static final Logger logger = Logger.getLogger(BeanUtils.class);
    
    /**
     * 
     * @return Bean fields for {@link #getBean()} class that have getter/setter
     *         pairs. For instance, for the JavaBean like shown below
     *         {@link #getBeanFieldNames()} will return string array like this:
     *         {"Int", "String"}.
     * 
     * <pre>
     * class Bean {
     *     private int intValue;
     * 
     *     private String stringValue;
     * 
     *     public int getInt() {
     *         return intValue;
     *     }
     * 
     *     public void setInt(String value) {
     *         intValue = value;
     *     }
     * 
     *     public String getString() {
     *         return stringValue;
     *     }
     * 
     *     public void setString(String value) {
     *         stringValue = value;
     *     }
     * }
     * </pre>
     */
    public static List<String> getBeanFieldNames(Class beanClass) {
        List<String> beanFields = new ArrayList<String>();
        
        Method[] methods = beanClass.getMethods();
        
        for (Method method : methods) {
            if (method.getName().startsWith("get")
                    //    Boolean getters may also start with "is" prefix
                    || (isReturnTypeBoolean(method) 
                        && (method.getName().startsWith("is")))) {
                
                String nameSuffix = method.getName().startsWith("is") ?
                        method.getName().substring(2) : method.getName().substring(3);
                
                if (logger.isDebugEnabled()) {
                    logger.debug(MessageFormat.format("Found getter {0} for type {1}", 
                            method.getName(), beanClass.getName()));
                }
                
                Method setter = findSetterByGetter(methods, method, nameSuffix);
                
                if (setter != null) {
                    if (logger.isDebugEnabled()) {
                        logger.debug(MessageFormat.format("Found setter {0} for type {1}", 
                                setter.getName(), beanClass.getName()));
                    }
                    
                    beanFields.add(nameSuffix);
                }
            }
        }
            
        return beanFields; 
    }
    
    private static boolean isReturnTypeBoolean(Method method) {
        return method.getReturnType().equals(boolean.class)
            || method.getReturnType().equals(Boolean.class);
    }
    
    /**
     * 
     * @param methods TODO
     * @param getter
     * @param nameSuffix
     *            nameSuffix = <code>getter.getName().substring(3)</code>
     * @return Corresponding setter method or null if no setter founded.
     */
    private static Method findSetterByGetter(Method[] methods, Method getter, String nameSuffix) {
        for (Method method : methods) {
            if (method.getName().equals("set" + nameSuffix)) {
                Class[] parameterTypes = method.getParameterTypes();
                
                if ((parameterTypes != null) 
                    && (parameterTypes.length == 1)
                    && (parameterTypes[0].equals(getter.getReturnType()))) 
                {
                    return method;
                }
            }
        }
        
        return null;
    }
    
    public static Class getBeanFieldType(Class clazz, String beanFieldName) {
        Method getter = getBeanFieldGetterMethod(clazz, beanFieldName);
        return getter.getReturnType();
    }
    
    private static Method getBeanFieldGetterMethod(Class clazz, String beanFieldName) {
        //  FIXME 
        
        try {
            return clazz.getMethod("get" + beanFieldName, new Class[] {});
        } catch (Exception e) {
            
            if (logger.isDebugEnabled()) {
                logger.debug(
                    MessageFormat.format("Type {0} missing public method get{1}",
                            clazz.getName(), beanFieldName));
            }
            
            try {
                return clazz.getMethod("is" + beanFieldName, new Class[] {});
            } catch (Exception e2) {
                throw new ApplicationRuntimeException(MessageFormat.format("Type {0} missing public method is{1}",
                        clazz.getName(), beanFieldName)); 
            }
        }
    }
    
    public static Object getBeanFieldValue(Object bean, String beanFieldName) {
        try {
            return getBeanFieldGetterMethod(bean.getClass(), beanFieldName).invoke(bean, new Object[]{});
        } catch (Exception e) {
            throw new ApplicationRuntimeException(e.getMessage(), e);
        }
    }
    
    private static Method getBeanFieldSetterMethod(Object bean, String beanFieldName, Class clazz) {
        try {
            return bean.getClass().getMethod("set" + beanFieldName, new Class[] {clazz});
        } catch (Exception e) {
            throw new ApplicationRuntimeException(
                    MessageFormat.format("Type {0} missing required public method set{1}",
                    bean.getClass().getName(), beanFieldName));
        }
    }
    
    public static void setBeanFieldValue(Object bean, String beanFieldName, Object value, Class clazz) {
        try {
            if (! isEnum(clazz)) {
                getBeanFieldSetterMethod(bean, beanFieldName, clazz).invoke(bean, value);
            } else {
                //  Convert value to corresponding enum type
                @SuppressWarnings("unchecked")
                Object enumValue = value == null ? null : Enum.valueOf(clazz, value.toString());
                getBeanFieldSetterMethod(bean, beanFieldName, clazz).invoke(bean, enumValue);
            }
        } catch (Exception e) {
            throw new ApplicationRuntimeException(e.getMessage(), e);
        }
    }

    public static boolean isSimpleType(Class clazz) {
        return isInt(clazz)
            || isBoolean(clazz)
            || isString(clazz)
            || isBigDecimal(clazz)
            || isDate(clazz)
            || isEnum(clazz)
            || isDouble(clazz);
    }
    
    public static boolean isInt(Class clazz) {
        return int.class.equals(clazz);
    }
    
    public static boolean isBoolean(Class clazz) {
        return boolean.class.equals(clazz);
    }
    
    public static boolean isString(Class clazz) {
        return String.class.equals(clazz);
    }
    
    public static boolean isBigDecimal(Class clazz) {
        return java.math.BigDecimal.class.equals(clazz);
    }
    
    /** {@link java.util.Date} */
    public static boolean isDate(Class clazz) {
        return java.util.Date.class.equals(clazz);
    }

    public static boolean isList(Class clazz) {
        return java.util.List.class.equals(clazz);
    }
    
    public static boolean isEnum(Class clazz) {
        return clazz.isEnum();
    }

    public static boolean isDouble(Class clazz) {
        return double.class.equals(clazz);
    }

    public static String getValueAsString(Object value) {
        if (value == null) {
            return null;
        }
        if (value.getClass().isEnum()) {
            //  Do not cast to JAXB's @XmlEnum directly so that
            //  no compilation requirements appeared to JAXB API
            for (Annotation a : value.getClass().getAnnotations()) {
                if (a.annotationType().getName().equals("javax.xml.bind.annotation.XmlEnum")) {
                    //  JAXB 2.0 @XmlEnum
                    try {
                        Method valueMethod = value.getClass().getMethod("value");
                        return (String) valueMethod.invoke(value);
                    } catch (Exception e) {
                        BeanUtils.logger.warn(e);
                        break;
                    }
                }
            }
        }
        return value.toString();
    }
}
